/*file chiron src/jcoglan.com/bluff.js */

/**
    a line chart library by James Coglan, <http://bluff.jcoglan.com/>, based
    on Ruby's Gruff.
*/

/*preamble-1

    Copyright (c) 2008 James Coglan
    MIT License

*/

/*preamble

    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    The license terms are stated in full in <license.rst> and at the end
    of all source files.

*/

// This is the version of Bluff you are using.
exports.VERSION = '0.3.4';
  
include('google.com/excanvas.js');
include('ruby/class.js');

exports.array = function(list) {
  if (list.length === undefined) return [list];
  var ary = [], i = list.length;
  while (i--) ary[i] = list[i];
  return ary;
};
  
exports.each = function(list, block, context) {
  for (var i = 0, n = list.length; i < n; i++) {
    block.call(context || null, list[i], i);
  }
};
  
exports.reverse_each = function(list, block, context) {
  var i = list.length;
  while (i--) block.call(context || null, list[i], i);
};

exports.sum = function(list) {
  var sum = 0, i = list.length;
  while (i--) sum += list[i];
  return sum;
};

exports.Base = new Class({
  extend: {
    // Draw extra lines showing where the margins and text centers are
    DEBUG: false,
    
    // Used for navigating the array of data to plot
    DATA_LABEL_INDEX: 0,
    DATA_VALUES_INDEX: 1,
    DATA_COLOR_INDEX: 2,

    // Space around text elements. Mostly used for vertical spacing
    LEGEND_MARGIN: 10,
    TITLE_MARGIN: 10,
    LABEL_MARGIN: 10,

    DEFAULT_TARGET_WIDTH:  800
  },
  
  // Blank space above the graph
  top_margin: null,

  // Blank space below the graph
  bottom_margin: null,

  // Blank space to the right of the graph
  right_margin: null,

  // Blank space to the left of the graph
  left_margin: null,

  // A hash of names for the individual columns, where the key is the array
  // index for the column this label represents.
  //
  // Not all columns need to be named.
  //
  // Example: {0: 2005, 3: 2006, 5: 2007, 7: 2008}
  labels: null,

  // Used internally for spacing.
  //
  // By default, labels are centered over the point they represent.
  center_labels_over_point: null,

  // Used internally for horizontal graph types.
  has_left_labels: null,

  // A label for the bottom of the graph
  x_axis_label: null,

  // A label for the left side of the graph
  y_axis_label: null,

  // x_axis_increment: null,

  // Manually set increment of the horizontal marking lines
  y_axis_increment: null,

  // Get or set the list of colors that will be used to draw the bars or lines.
  colors: null,

  // The large title of the graph displayed at the top
  title: null,

  // Font used for titles, labels, etc.
  font: null,

  font_color: null,

  // Prevent drawing of line markers
  hide_line_markers: null,

  // Prevent drawing of the legend
  hide_legend: null,

  // Prevent drawing of the title
  hide_title: null,

  // Prevent drawing of line numbers
  hide_line_numbers: null,

  // Message shown when there is no data. Fits up to 20 characters. Defaults
  // to "No Data."
  no_data_message: null,

  // The font size of the large title at the top of the graph
  title_font_size: null,

  // Optionally set the size of the font. Based on an 800x600px graph.
  // Default is 20.
  //
  // Will be scaled down if graph is smaller than 800px wide.
  legend_font_size: null,

  // The font size of the labels around the graph
  marker_font_size: null,

  // The color of the auxiliary lines
  marker_color: null,

  // The number of horizontal lines shown for reference
  marker_count: null,

  // You can manually set a minimum value instead of having the values
  // guessed for you.
  //
  // Set it after you have given all your data to the graph object.
  minimum_value: null,

  // You can manually set a maximum value, such as a percentage-based graph
  // that always goes to 100.
  //
  // If you use this, you must set it after you have given all your data to
  // the graph object.
  maximum_value: null,

  // Set to false if you don't want the data to be sorted with largest avg
  // values at the back.
  sort: null,

  // Experimental
  additional_line_values: null,

  // Experimental
  stacked: null,

  // Optionally set the size of the colored box by each item in the legend.
  // Default is 20.0
  //
  // Will be scaled down if graph is smaller than 800px wide.
  legend_box_size: null,
  
  // If one numerical argument is given, the graph is drawn at 4/3 ratio
  // according to the given width (800 results in 800x600, 400 gives 400x300,
  // etc.).
  //
  // Or, send a geometry string for other ratios ('800x400', '400x225').
  initialize: function(renderer, target_width) {
    this._d = new Renderer(renderer);
    target_width = target_width || this.klass.DEFAULT_TARGET_WIDTH;
    
    this.top_margin = this.bottom_margin =
    this.left_margin = this.right_margin = 20;
    
    var geo;
    
    if (typeof target_width != 'number') {
      geo = target_width.split('x');
      this._columns = parseFloat(geo[0]);
      this._rows = parseFloat(geo[1]);
    } else {
      this._columns = parseFloat(target_width);
      this._rows = this._columns * 0.75;
    }
    
    this.initialize_ivars();
    
    this._reset_themes();
    this.theme_keynote();
  },
  
  // Set instance variables for this object.
  //
  // Subclasses can override this, call super, then set values separately.
  //
  // This makes it possible to set defaults in a subclass but still allow
  // developers to change this values in their program.
  initialize_ivars: function() {
    // Internal for calculations
    this._raw_columns = 800;
    this._raw_rows = 800 * (this._rows/this._columns);
    this._column_count = 0;
    this.marker_count = null;
    this.maximum_value = this.minimum_value = null;
    this._has_data = false;
    this._data = [];
    this.labels = {};
    this._labels_seen = {};
    this.sort = true;
    this.title = null;

    this._scale = this._columns / this._raw_columns;

    this.marker_font_size = 21.0;
    this.legend_font_size = 20.0;
    this.title_font_size = 36.0;
    
    this.legend_box_size = 20.0;

    this.no_data_message = "No Data";

    this.hide_line_markers = this.hide_legend = this.hide_title = this.hide_line_numbers = false;
    this.center_labels_over_point = true;
    this.has_left_labels = false;

    this.additional_line_values = [];
    this._additional_line_colors = [];
    this._theme_options = {};
    
    this.x_axis_label = this.y_axis_label = null;
    this.y_axis_increment = null;
    this.stacked = null;
    this._norm_data = null;
  },
  
  // Sets the top, bottom, left and right margins to +margin+.
  set_margins: function(margin) {
    this.top_margin = this.left_margin = this.right_margin = this.bottom_margin = margin;
  },

  // Sets the font for graph text to the font at +font_path+.
  set_font: function(font_path) {
    this.font = font_path;
    this._d.font = this.font;
  },

  // Add a color to the list of available colors for lines.
  //
  // Example:
  //  add_color('#c0e9d3')
  add_color: function(colorname) {
    this.colors.push(colorname);
  },

  // Replace the entire color list with a new array of colors. You need to
  // have one more color than the number of datasets you intend to draw. Also
  // aliased as the colors= setter method.
  //
  // Example:
  //  replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
  replace_colors: function(color_list) {
    this.colors = color_list || [];
  },

  // You can set a theme manually. Assign a hash to this method before you
  // send your data.
  //
  //  graph.set_theme({
  //    colors: ['orange', 'purple', 'green', 'white', 'red'],
  //    marker_color: 'blue',
  //    background_colors: ['black', 'grey']
  //  })
  //
  // background_image: 'squirrel.png' is also possible.
  //
  // (Or hopefully something better looking than that.)
  //
  set_theme: function(options) {
    this._reset_themes();
    
    this._theme_options = {
      colors: ['black', 'white'],
      additional_line_colors: [],
      marker_color: 'white',
      font_color: 'black',
      background_colors: null,
      background_image: null
    };
    for (var key in options) this._theme_options[key] = options[key];

    this.colors = this._theme_options.colors;
    this.marker_color = this._theme_options.marker_color;
    this.font_color = this._theme_options.font_color || this.marker_color;
    this._additional_line_colors = this._theme_options.additional_line_colors;
    
    this._render_background();
  },

  // A color scheme similar to the popular presentation software.
  theme_keynote: function() {
    // Colors
    this._blue = '#6886B4';
    this._yellow = '#FDD84E';
    this._green = '#72AE6E';
    this._red = '#D1695E';
    this._purple = '#8A6EAF';
    this._orange = '#EFAA43';
    this._white = 'white';
    this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._white];

    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['black', '#4a465a']
    });
  },

  // A color scheme plucked from the colors on the popular usability blog.
  theme_37signals: function() {
    // Colors
    this._green = '#339933';
    this._purple = '#cc99cc';
    this._blue = '#336699';
    this._yellow = '#FFF804';
    this._red = '#ff0000';
    this._orange = '#cf5910';
    this._black = 'black';
    this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._black];

    this.set_theme({
      colors: this.colors,
      marker_color: 'black',
      font_color: 'black',
      background_colors: ['#d1edf5', 'white']
    });
  },

  // A color scheme from the colors used on the 2005 Rails keynote
  // presentation at RubyConf.
  theme_rails_keynote: function() {
    // Colors
    this._green = '#00ff00';
    this._grey = '#333333';
    this._orange = '#ff5d00';
    this._red = '#f61100';
    this._white = 'white';
    this._light_grey = '#999999';
    this._black = 'black';
    this.colors = [this._green, this._grey, this._orange, this._red, this._white, this._light_grey, this._black];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['#0083a3', '#0083a3']
    });
  },

  // A color scheme similar to that used on the popular podcast site.
  theme_odeo: function() {
    // Colors
    this._grey = '#202020';
    this._white = 'white';
    this._dark_pink = '#a21764';
    this._green = '#8ab438';
    this._light_grey = '#999999';
    this._dark_blue = '#3a5b87';
    this._black = 'black';
    this.colors = [this._grey, this._white, this._dark_blue, this._dark_pink, this._green, this._light_grey, this._black];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['#ff47a4', '#ff1f81']
    });
  },

  // A pastel theme
  theme_pastel: function() {
    // Colors
    this.colors = [
        '#a9dada', // blue
        '#aedaa9', // green
        '#daaea9', // peach
        '#dadaa9', // yellow
        '#a9a9da', // dk purple
        '#daaeda', // purple
        '#dadada' // grey
      ];
    
    this.set_theme({
      colors: this.colors,
      marker_color: '#aea9a9', // Grey
      font_color: 'black',
      background_colors: 'white'
    });
  },

  // A greyscale theme
  theme_greyscale: function() {
    // Colors
    this.colors = [
        '#282828', // 
        '#383838', // 
        '#686868', // 
        '#989898', // 
        '#c8c8c8', // 
        '#e8e8e8' // 
      ];
    
    this.set_theme({
      colors: this.colors,
      marker_color: '#aea9a9', // Grey
      font_color: 'black',
      background_colors: 'white'
    });
  },
  
  // Parameters are an array where the first element is the name of the dataset
  // and the value is an array of values to plot.
  //
  // Can be called multiple times with different datasets for a multi-valued
  // graph.
  //
  // If the color argument is nil, the next color from the default theme will
  // be used.
  //
  // NOTE: If you want to use a preset theme, you must set it before calling
  // data().
  //
  // Example:
  //   data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
  data: function(name, data_points, color) {
    data_points = (data_points === undefined) ? [] : data_points;
    color = color || null;
    
    data_points = array(data_points); // make sure it's an array
    this._data.push([name, data_points, (color || this._increment_color())]);
    // Set column count if this is larger than previous counts
    this._column_count = (data_points.length > this._column_count) ? data_points.length : this._column_count;

    // Pre-normalize
    each(data_points, function(data_point, index) {
      if (!data_point) return;
      
      // Setup max/min so spread starts at the low end of the data points
      if (this.maximum_value === null && this.minimum_value === null)
        this.maximum_value = this.minimum_value = data_point;
      
      // TODO Doesn't work with stacked bar graphs
      // Original: @maximum_value = _larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
      this.maximum_value = this._larger_than_max(data_point) ? data_point : this.maximum_value;
      if (this.maximum_value > 0) this._has_data = true;
      
      this.minimum_value = this._less_than_min(data_point) ? data_point : this.minimum_value;
      if (this.minimum_value < 0) this._has_data = true;
    }, this);
  },
  
  // Overridden by subclasses to do the actual plotting of the graph.
  //
  // Subclasses should start by calling super() for this method.
  draw: function() {
    if (this.stacked) this._make_stacked();
    this._setup_drawing();
    
    this._debug(function() {
      // Outer margin
      this._d.rectangle(this.left_margin, this.top_margin,
                                  this._raw_columns - this.right_margin, this._raw_rows - this.bottom_margin);
      // Graph area box
      this._d.rectangle(this._graph_left, this._graph_top, this._graph_right, this._graph_bottom);
    });
  },
  
  clear: function() {
    this._render_background();
  },
  
  // Calculates size of drawable area and draws the decorations.
  //
  // * line markers
  // * legend
  // * title
  _setup_drawing: function() {
    // Maybe should be done in one of the following functions for more granularity.
    if (!this._has_data) return this._draw_no_data();
    
    this._normalize();
    this._setup_graph_measurements();
    if (this.sort) this._sort_norm_data();
    
    this._draw_legend();
    this._draw_line_markers();
    this._draw_axis_labels();
    this._draw_title();
  },
  
  // Make copy of data with values scaled between 0-100
  _normalize: function(force) {
    if (this._norm_data === null || force === true) {
      this._norm_data = [];
      if (!this._has_data) return;
      
      this._calculate_spread();
      
      each(this._data, function(data_row) {
        var norm_data_points = [];
        each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
          if (data_point === null || data_point === undefined)
            norm_data_points.push(null);
          else
            norm_data_points.push((data_point - this.minimum_value) / this._spread);
        }, this);
        this._norm_data.push([data_row[this.klass.DATA_LABEL_INDEX], norm_data_points, data_row[this.klass.DATA_COLOR_INDEX]]);
      }, this);
    }
  },
  
  _calculate_spread: function() {
    this._spread = this.maximum_value - this.minimum_value;
    this._spread = this._spread > 0 ? this._spread : 1;
  },
  
  // Calculates size of drawable area, general font dimensions, etc.
  _setup_graph_measurements: function() {
    this._marker_caps_height = this.hide_line_markers ? 0 :
                                this._calculate_caps_height(this.marker_font_size);
    this._title_caps_height = this.hide_title ? 0 :
                                this._calculate_caps_height(this.title_font_size);
    this._legend_caps_height = this.hide_legend ? 0 :
                                this._calculate_caps_height(this.legend_font_size);
    
    var longest_label,
        longest_left_label_width,
        line_number_width,
        last_label,
        extra_room_for_long_label,
        x_axis_label_height,
        key;
    
    if (this.hide_line_markers) {
      this._graph_left = this.left_margin;
      this._graph_right_margin = this.right_margin;
      this._graph_bottom_margin = this.bottom_margin;
    } else {
      longest_left_label_width = 0;
      if (this.has_left_labels) {
        longest_label = '';
        for (key in this.labels) {
          longest_label = longest_label.length > this.labels[key].length
              ? longest_label
              : this.labels[key];
        }
        longest_left_label_width = this._calculate_width(this.marker_font_size, longest_label) * 1.25;
      } else {
        longest_left_label_width = this._calculate_width(this.marker_font_size, this._label(this.maximum_value));
      }
      
      // Shift graph if left line numbers are hidden
      line_number_width = this.hide_line_numbers && !this.has_left_labels
                            ? 0
                            : longest_left_label_width + this.klass.LABEL_MARGIN * 2;
      
      this._graph_left = this.left_margin +
                         line_number_width +
                         (this.y_axis_label === null ? 0.0 : this._marker_caps_height + this.klass.LABEL_MARGIN * 2);
      // Make space for half the width of the rightmost column label.
      // Might be greater than the number of columns if between-style bar markers are used.
      last_label = -Infinity;
      for (key in this.labels)
        last_label = last_label > Number(key) ? last_label : Number(key);
      last_label = Math.round(last_label);
      extra_room_for_long_label = (last_label >= (this._column_count-1) && this.center_labels_over_point)
          ? this._calculate_width(this.marker_font_size, this.labels[last_label]) / 2
          : 0.0;
      this._graph_right_margin  = this.right_margin + extra_room_for_long_label;
      
      this._graph_bottom_margin = this.bottom_margin +
                                  this._marker_caps_height + this.klass.LABEL_MARGIN;
    }
    
    this._graph_right = this._raw_columns - this._graph_right_margin;
    this._graph_width = this._raw_columns - this._graph_left - this._graph_right_margin;
    
    // When hide_title, leave a TITLE_MARGIN space for aesthetics.
    // Same with hide_legend
    this._graph_top = this.top_margin +
                        (this.hide_title ? this.klass.TITLE_MARGIN : this._title_caps_height + this.klass.TITLE_MARGIN * 2) +
                        (this.hide_legend ? this.klass.LEGEND_MARGIN : this._legend_caps_height + this.klass.LEGEND_MARGIN * 2);
    
    x_axis_label_height = (this.x_axis_label === null) ? 0.0 :
                            this._marker_caps_height + this.klass.LABEL_MARGIN;
    this._graph_bottom = this._raw_rows - this._graph_bottom_margin - x_axis_label_height;
    this._graph_height = this._graph_bottom - this._graph_top;
  },
  
  // Draw the optional labels for the x axis and y axis.
  _draw_axis_labels: function() {
    if (this.x_axis_label) {
      // X Axis
      // Centered vertically and horizontally by setting the
      // height to 1.0 and the width to the width of the graph.
      var x_axis_label_y_coordinate = this._graph_bottom + this.klass.LABEL_MARGIN * 2 + this._marker_caps_height;
      
      // TODO Center between graph area
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke = 'transparent';
      this._d.pointsize = this._scale_fontsize(this.marker_font_size);
      this._d.gravity = 'north';
      this._d.annotate_scaled(
                    this._raw_columns, 1.0,
                    0.0, x_axis_label_y_coordinate,
                    this.x_axis_label, this._scale);
      this._debug(function() {
        this._d.line(0.0, x_axis_label_y_coordinate, this._raw_columns, x_axis_label_y_coordinate);
      });
    }
    
    // TODO Y label (not generally possible in browsers)
  },
  
  // Draws horizontal background lines and labels
  _draw_line_markers: function() {
    if (this.hide_line_markers) return;
    
    if (this.y_axis_increment === null) {
      // Try to use a number of horizontal lines that will come out even.
      //
      // TODO Do the same for larger numbers...100, 75, 50, 25
      if (this.marker_count === null) {
        each([3,4,5,6,7], function(lines) {
          if (!this.marker_count && this._spread % lines == 0)
            this.marker_count = lines;
        }, this);
        this.marker_count = this.marker_count || 4;
      }
      this._increment = (this._spread > 0) ? this._significant(this._spread / this.marker_count) : 1;
    } else {
      // TODO Make this work for negative values
      this.maximum_value = Math.max(Math.ceil(this.maximum_value), this.y_axis_increment);
      this.minimum_value = Math.floor(this.minimum_value);
      this._calculate_spread();
      this._normalize(true);
      
      this.marker_count = Math.round(this._spread / this.y_axis_increment);
      this._increment = this.y_axis_increment;
    }
    this._increment_scaled = this._graph_height / (this._spread / this._increment);
    
    // Draw horizontal line markers and annotate with numbers
    var index, n, y, marker_label;
    for (index = 0, n = this.marker_count; index <= n; index++) {
      y = this._graph_top + this._graph_height - index * this._increment_scaled;
      
      this._d.stroke = this.marker_color;
      this._d.stroke_width = 1;
      this._d.line(this._graph_left, y, this._graph_right, y);
      
      marker_label = index * this._increment + this.minimum_value;
      
      if (!this.hide_line_numbers) {
        this._d.fill = this.font_color;
        if (this.font) this._d.font = this.font;
        this._d.stroke = 'transparent';
        this._d.pointsize = this._scale_fontsize(this.marker_font_size);
        this._d.gravity = 'east';
        
        // Vertically center with 1.0 for the height
        this._d.annotate_scaled(this._graph_left - this.klass.LABEL_MARGIN, 1.0, 0.0, y, this._label(marker_label), this._scale);
      }
    }
  },
  
  _center: function(size) {
    return (this._raw_columns - size) / 2;
  },
  
  // Draws a legend with the names of the datasets matched to the colors used
  // to draw them.
  _draw_legend: function() {
    if (this.hide_legend) return;
    
    this._legend_labels = [];
    for (var i = 0, n = this._data.length; i < n; i++)
      this._legend_labels.push(this._data[i][this.klass.DATA_LABEL_INDEX]);
    
    var legend_square_width = this.legend_box_size; // small square with color of this item
    
    // May fix legend drawing problem at small sizes
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this.legend_font_size;
    
    var label_widths = [[]]; // Used to calculate line wrap
    each(this._legend_labels, function(label) {
      var last = label_widths.length - 1;
      var metrics = this._d.get_type_metrics(label);
      var label_width = metrics.width + legend_square_width * 2.7;
      label_widths[last].push(label_width);
      
      if (sum(label_widths[last]) > (this._raw_columns * 0.9))
        label_widths.push([label_widths[last].pop()]);
    }, this);
    
    var current_x_offset = this._center(sum(label_widths[0]));
    var current_y_offset = this.hide_title ?
                            this.top_margin + this.klass.LEGEND_MARGIN :
                            this.top_margin +
                            this.klass.TITLE_MARGIN + this._title_caps_height +
                            this.klass.LEGEND_MARGIN;
    
    this._debug(function() {
      this._d.stroke_width = 1;
      this._d.line(0, current_y_offset, this._raw_columns, current_y_offset);
    });
    
    each(this._legend_labels, function(legend_label, index) {
      
      // Draw label
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.pointsize = this._scale_fontsize(this.legend_font_size);
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.gravity = 'west';
      this._d.annotate_scaled(this._raw_columns, 1.0,
                              current_x_offset + (legend_square_width * 1.7), current_y_offset,
                              legend_label, this._scale);
      
      // Now draw box with color of this dataset
      this._d.stroke = 'transparent';
      this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
      this._d.rectangle(current_x_offset,
                        current_y_offset - legend_square_width / 2.0,
                        current_x_offset + legend_square_width,
                        current_y_offset + legend_square_width / 2.0);
      
      this._d.pointsize = this.legend_font_size;
      var metrics = this._d.get_type_metrics(legend_label);
      var current_string_offset = metrics.width + (legend_square_width * 2.7),
          line_height;
      
      // Handle wrapping
      label_widths[0].shift();
      if (label_widths[0].length == 0) {
        this._debug(function() {
          this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
        });
        
        label_widths.shift();
        if (label_widths.length > 0) current_x_offset = this._center(sum(label_widths[0]));
        line_height = Math.max(this._legend_caps_height, legend_square_width) + this.klass.LEGEND_MARGIN;
        if (label_widths.length > 0) {
          // Wrap to next line and shrink available graph dimensions
          current_y_offset += line_height;
          this._graph_top += line_height;
          this._graph_height = this._graph_bottom - this._graph_top;
        }
      } else {
        current_x_offset += current_string_offset;
      }
    }, this);
    this._color_index = 0;
  },
  
  // Draws a title on the graph.
  _draw_title: function() {
    if (this.hide_title || !this.title) return;
    
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.title_font_size);
    this._d.font_weight = 'bold';
    this._d.gravity = 'north';
    this._d.annotate_scaled(this._raw_columns, 1.0, 0, this.top_margin, this.title, this._scale);
  },
  
  // Draws column labels below graph, centered over x_offset
  //--
  // TODO Allow WestGravity as an option
  _draw_label: function(x_offset, index) {
    if (this.hide_line_markers) return;
    
    var y_offset;
    
    if (this.labels[index] && !this._labels_seen[index]) {
      y_offset = this._graph_bottom + this.klass.LABEL_MARGIN;
      
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.pointsize = this._scale_fontsize(this.marker_font_size);
      this._d.gravity = 'north';
      this._d.annotate_scaled(1.0, 1.0, x_offset, y_offset, this.labels[index], this._scale);
      this._labels_seen[index] = true;
      
      this._debug(function() {
        this._d.stroke_width = 1;
        this._d.line(0.0, y_offset, this._raw_columns, y_offset);
      });
    }
  },
  
  // Shows an error message because you have no data.
  _draw_no_data: function() {
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.stroke = 'transparent';
    this._d.font_weight = 'normal';
    this._d.pointsize = this._scale_fontsize(80);
    this._d.gravity = 'center';
    this._d.annotate_scaled(this._raw_columns, this._raw_rows/2, 0, 10, this.no_data_message, this._scale);
  },
  
  // Finds the best background to render based on the provided theme options.
  _render_background: function() {
    var colors = this._theme_options.background_colors;
    switch (true) {
      case colors instanceof Array:
        this._render_gradiated_background.apply(this, colors);
        break;
      case typeof colors == 'string':
        this._render_solid_background(colors);
        break;
      default:
        this._render_image_background(this._theme_options.background_image);
        break;
    }
  },
  
  // Make a new image at the current size with a solid +color+.
  _render_solid_background: function(color) {
    this._d.render_solid_background(this._columns, this._rows, color);
  },

  // Use with a theme definition method to draw a gradiated background.
  _render_gradiated_background: function(top_color, bottom_color) {
    this._d.render_gradiated_background(this._columns, this._rows, top_color, bottom_color);
  },

  // Use with a theme to use an image (800x600 original) background.
  _render_image_background: function(image_path) {
    // TODO
  },
  
  // Resets everything to defaults (except data).
  _reset_themes: function() {
    this._color_index = 0;
    this._labels_seen = {};
    this._theme_options = {};
    this._d.scale(this._scale, this._scale);
  },
  
  _scale_value: function(value) {
    return this._scale * value;
  },
  
  // Return a comparable fontsize for the current graph.
  _scale_fontsize: function(value) {
    var new_fontsize = value * this._scale;
    return new_fontsize;
  },
  
  _clip_value_if_greater_than: function(value, max_value) {
    return (value > max_value) ? max_value : value;
  },

  // Overridden by subclasses such as stacked bar.
  _larger_than_max: function(data_point, index) {
    return data_point > this.maximum_value;
  },

  _less_than_min: function(data_point, index) {
    return data_point < this.minimum_value;
  },

  // Overridden by subclasses that need it.
  _max: function(data_point, index) {
    return data_point;
  },

  // Overridden by subclasses that need it.
  _min: function(data_point, index) {
    return data_point;
  },
  
  _significant: function(inc) {
    if (inc == 0) return 1.0;
    var factor = 1.0;
    while (inc < 10) {
      inc *= 10;
      factor /= 10;
    }
    
    while (inc > 100) {
      inc /= 10;
      factor *= 10;
    }
    
    return Math.floor(inc) * factor;
  },
  
  // Sort with largest overall summed value at front of array so it shows up
  // correctly in the drawn graph.
  _sort_norm_data: function() {
    var sums = this._sums, index = this.klass.DATA_VALUES_INDEX;
    this._norm_data.sort(function(a,b) {
      return sums(b[index]) - sums(a[index]);
    });
  },
  
  _sums: function(data_set) {
    var total_sum = 0;
    each(data_set, function(num) { total_sum += num });
    return total_sum;
  },
  
  _make_stacked: function() {
    var stacked_values = [], i = this._column_count;
    while (i--) stacked_values[i] = 0;
    each(this._data, function(value_set) {
      each(value_set[this.klass.DATA_VALUES_INDEX], function(value, index) {
        stacked_values[index] += value;
      }, this);
      value_set[this.klass.DATA_VALUES_INDEX] = array(stacked_values);
    }, this);
  },
  
  // Takes a block and draws it if DEBUG is true.
  //
  // Example:
  //   debug { @d.rectangle x1, y1, x2, y2 }
  _debug: function(block) {
    if (this.klass.DEBUG) {
      this._d.fill = 'transparent';
      this._d.stroke = 'turquoise';
      block.call(this);
    }
  },
  
  _increment_color: function() {
    if (this._color_index == 0) {
      this._color_index += 1;
      return this.colors[0];
    } else {
      if (this._color_index < this.colors.length) {
        this._color_index += 1;
        return this.colors[this._color_index - 1];
      } else {
        // Start over
        this._color_index = 0;
        return this.colors[this.colors.length - 1];
      }
    }
  },
  
  // Return a formatted string representing a number value that should be
  // printed as a label.
  _label: function(value) {
    if (this._spread % this.marker_count == 0 || this.y_axis_increment !== null) {
      return String(Math.round(value));
    }
    
    if (this._spread > 10)
      return String(Math.floor(value));
    else if (this._spread >= 3)
      return String(Math.floor(value * 100) / 100);
    else
      return String(value);
  },
  
  // Returns the height of the capital letter 'X' for the current font and
  // size.
  //
  // Not scaled since it deals with dimensions that the regular scaling will
  // handle.
  _calculate_caps_height: function(font_size) {
    return this._d.caps_height(font_size);
  },
  
  // Returns the width of a string at this pointsize.
  //
  // Not scaled since it deals with dimensions that the regular 
  // scaling will handle.
  _calculate_width: function(font_size, text) {
    return this._d.text_width(font_size, text);
  }
});


exports.Area = new Class(Base, {
  
  draw: function() {
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    this._d.stroke = 'transparent';
    
    each(this._norm_data, function(data_row) {
      var poly_points = [];
      var prev_x = 0.0, prev_y = 0.0;
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        // Use incremented x and scaled y
        var new_x = this._graph_left + (this._x_increment * index);
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
        
        if (prev_x > 0 && prev_y > 0) {
          poly_points.push(new_x);
          poly_points.push(new_y);
          
          // this._d.polyline(prev_x, prev_y, new_x, new_y);
        } else {
          poly_points.push(this._graph_left);
          poly_points.push(this._graph_bottom - 1);
          poly_points.push(new_x);
          poly_points.push(new_y);
          
          // this._d.polyline(this._graph_left, this._graph_bottom, new_x, new_y);
        }
        
        this._draw_label(new_x, index);
        
        prev_x = new_x;
        prev_y = new_y;
      }, this);
      
      // Add closing points, draw polygon
      poly_points.push(this._graph_right);
      poly_points.push(this._graph_bottom - 1);
      poly_points.push(this._graph_left);
      poly_points.push(this._graph_bottom - 1);
      
      this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
      this._d.polyline(poly_points);
      
    }, this);
  }
});


//	This class perfoms the y coordinats conversion for the bar class.
//
//	There are three cases: 
//
//    1. Bars all go from zero in positive direction
//		2. Bars all go from zero to negative direction	
//		3. Bars either go from zero to positive or from zero to negative
//
exports.BarConversion = new Class({
	mode: null,
	zero: null,
	graph_top: null,
	graph_height: null,
	minimum_value: null,
	spread: null,
	
	getLeftYRightYscaled: function(data_point, result) {
	  var val;
		switch (this.mode) {
		  case 1: // Case one
			  // minimum value >= 0 ( only positiv values )
        result[0] = this.graph_top + this.graph_height*(1 - data_point) + 1;
    		result[1] = this.graph_top + this.graph_height - 1;
    		break;
		  case 2:  // Case two
			  // only negativ values
     		result[0] = this.graph_top + 1;
    		result[1] = this.graph_top + this.graph_height*(1 - data_point) - 1;
    		break;
		  case 3: // Case three
			  // positiv and negativ values
      	val = data_point-this.minimum_value/this.spread;
      	if ( data_point >= this.zero ) {
      		result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
	      	result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
      	} else {
				  result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
	      	result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
      	}
      	break;
		  default:
			  result[0] = 0.0;
			  result[1] = 0.0;
		}				
	}	

});


exports.Bar = new Class(Base, {
  
  draw: function() {
    // Labels will be centered over the left of the bar if
    // there are more labels than columns. This is basically the same 
    // as where it would be for a line graph.
    var labels = 0, key;
    for (key in this.labels) labels += 1;
    this.center_labels_over_point = (labels > this._column_count);
    
    this.callSuper();
    if (!this._has_data) return;

    this._draw_bars();
  },
  
  _draw_bars: function() {
    // Setup spacing.
    //
    // Columns sit side-by-side.
    var spacing_factor = 0.9; // space between the bars
    this._bar_width = this._graph_width / (this._column_count * this._data.length);
    var padding = (this._bar_width * (1 - spacing_factor)) / 2;

    this._d.stroke_opacity = 0.0;

    // Setup the BarConversion Object
    var conversion = new BarConversion();
    conversion.graph_height = this._graph_height;
    conversion.graph_top = this._graph_top;

    // Set up the right mode [1,2,3] see BarConversion for further explanation
    if (this.minimum_value >= 0) {
      // all bars go from zero to positiv
      conversion.mode = 1;
    } else {
      // all bars go from 0 to negativ
      if (this.maximum_value <= 0) {
        conversion.mode = 2;
      } else {
        // bars either go from zero to negativ or to positiv
        conversion.mode = 3;
        conversion.spread = this._spread;
        conversion.minimum_value = this.minimum_value;
        conversion.zero = -this.minimum_value/this._spread;
      }
    }

    // iterate over all normalised data
    each(this._norm_data, function(data_row, row_index) {

      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        // Use incremented x and scaled y
        // x
        var left_x = this._graph_left + (this._bar_width * (row_index + point_index + ((this._data.length - 1) * point_index))) + padding;
        var right_x = left_x + this._bar_width * spacing_factor;
        // y
        var conv = [];
        conversion.getLeftYRightYscaled(data_point, conv);

        // create new bar
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.stroke = 'transparent';
        this._d.rectangle(left_x, conv[0], right_x, conv[1]);

        // Calculate center based on bar_width and current row
        var label_center = this._graph_left + 
                      (this._data.length * this._bar_width * point_index) + 
                      (this._data.length * this._bar_width / 2.0) +
                      padding;
        // Subtract half a bar width to center left if requested
        this._draw_label(label_center - (this.center_labels_over_point ? this._bar_width / 2.0 : 0.0), point_index);
      }, this);

    }, this);

    // Draw the last label if requested
    if (this.center_labels_over_point)
      this._draw_label(this._graph_right, this._column_count);
  }
});


exports.Line = new Class(Base, {
  // Draw a dashed line at the given value
  baseline_value: null,
	
  // Color of the baseline
  baseline_color: null,
  
  // Hide parts of the graph to fit more datapoints, or for a different appearance.
  hide_dots: null,
  hide_lines: null,

  // Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
  //
  //  g = new Line('canvasId', 400) // 400px wide with lines
  //
  //  g = new Line('canvasId', 400, false) // 400px wide, no lines (for backwards compatibility)
  //
  //  g = new Line('canvasId', false) // Defaults to 800px wide, no lines (for backwards compatibility)
  // 
  // The preferred way is to call hide_dots or hide_lines instead.
  initialize: function(renderer) {
    if (arguments.length > 3) throw 'Wrong number of arguments';
    if (arguments.length == 1 || (typeof arguments[1] != 'number' && typeof arguments[1] != 'string'))
      this.callSuper(renderer, null);
    else
      this.callSuper();
    
    this.hide_dots = this.hide_lines = false;
    this.baseline_color = 'red';
    this.baseline_value = null;
  },
  
  draw: function() {
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Check to see if more than one datapoint was given. NaN can result otherwise.
    this.x_increment = (this._column_count > 1) ? (this._graph_width / (this._column_count - 1)) : this._graph_width;
    
    var level;
    
    if (this._norm_baseline !== undefined) {
      level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
      this._d.push();
      this._d.stroke = this.baseline_color;
      // TODO, opacity, dashes
      this._d.stroke_width = 3.0;
      this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
      this._d.pop();
    }
    
    each(this._norm_data, function(data_row) {
      var prev_x = null, prev_y = null;
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        var new_x = this._graph_left + (this.x_increment * index);
        if (data_point === undefined) return;
        
        this._draw_label(new_x, index);
        
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
        
        // Reset each time to avoid thin-line errors
        this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.stroke_opacity = 1.0;
        this._d.stroke_width = this._clip_value_if_greater_than(this._columns / (this._norm_data[0][1].length * 6), 3.0);
        
        if (!this.hide_lines && prev_x !== null && prev_y !== null)
          this._d.line(prev_x, prev_y, new_x, new_y);
        
        var circle_radius = this._clip_value_if_greater_than(this._columns / (this._norm_data[0][1].length * 2), 7.0);
        if (!this.hide_dots) this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
        
        prev_x = new_x;
        prev_y = new_y;
      }, this);
    }, this);
  },
  
  _normalize: function() {
    this.maximum_value = Math.max(this.maximum_value, this.baseline_value);
    this.callSuper();
    if (this.baseline_value !== null) this._norm_baseline = this.baseline_value / this.maximum_value;
  }
});


// Experimental!!! See also the Spider graph.
exports.Net = new Class(Base, {
  
  // Hide parts of the graph to fit more datapoints, or for a different appearance.
  hide_dots: null,
  
  initialize: function() {
    this.callSuper();
    
    this.hide_dots = false;
  },
  
  draw: function() {
    
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._radius = this._graph_height / 2.0;
    this._center_x = this._graph_left + (this._graph_width / 2.0);
    this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    var circle_radius = this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2.5), 7.0);
    
    this._d.stroke_opacity = 1.0;
    this._d.stroke_width = this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 4), 3.0);
    
    var level;
    
    if (this._norm_baseline !== undefined) {
      level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
      this._d.push();
      this._d.stroke_color  = this.baseline_color;
      this._d.fill_opacity = 0.0;
      // this._d.stroke_dasharray(10, 20);
      this._d.stroke_width = 5;
      this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
      this._d.pop();
    }
    
    each(this._norm_data, function(data_row) {
      var prev_x = null, prev_y = null;
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        if (data_point === undefined) return;
        
        var rad_pos = index * Math.PI * 2 / this._column_count,
            point_distance = data_point * this._radius,
            start_x = this._center_x + Math.sin(rad_pos) * point_distance,
            start_y = this._center_y - Math.cos(rad_pos) * point_distance,
            
            next_index = (index + 1 < data_row[this.klass.DATA_VALUES_INDEX].length) ? index + 1 : 0,
            
            next_rad_pos = next_index * Math.PI * 2 / this._column_count,
            next_point_distance = data_row[this.klass.DATA_VALUES_INDEX][next_index] * this._radius,
            end_x = this._center_x + Math.sin(next_rad_pos) * next_point_distance,
            end_y = this._center_y - Math.cos(next_rad_pos) * next_point_distance;
        
        this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.line(start_x, start_y, end_x, end_y);
        
        if (!this.hide_dots) this._d.circle(start_x, start_y, start_x - circle_radius, start_y);
      }, this);
    
    }, this);
  },
  
  // the lines connecting in the center, with the first line vertical
  _draw_line_markers: function() {
    if (this.hide_line_markers) return;
    
    // have to do this here (AGAIN)... see draw() in this class
    // because this funtion is called before the @radius, @center_x and @center_y are set
    this._radius = this._graph_height / 2.0;
    this._center_x = this._graph_left + (this._graph_width / 2.0);
    this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
    
    var rad_pos, marker_label;
    
    for (var index = 0, n = this._column_count; index < n; index++) {
      rad_pos = index * Math.PI * 2 / this._column_count;
      
      // Draw horizontal line markers and annotate with numbers
      this._d.stroke = this.marker_color;
      this._d.stroke_width = 1;
      
      this._d.line(this._center_x, this._center_y, this._center_x + Math.sin(rad_pos) * this._radius, this._center_y - Math.cos(rad_pos) * this._radius);
      
      marker_label = labels[index] ? labels[index] : '000';
      
      this._draw_label(this._center_x, this._center_y, rad_pos * 360 / (2 * Math.PI), this._radius, marker_label);
    }
  },
  
  _draw_label: function(center_x, center_y, angle, radius, amount) {
    var r_offset = 1.1,
        x_offset = center_x, // + 15 // The label points need to be tweaked slightly
        y_offset = center_y, // + 0  // This one doesn't though
        rad_pos = angle * Math.PI / 180,
        x = x_offset + (radius * r_offset * Math.sin(rad_pos)),
        y = y_offset - (radius * r_offset * Math.cos(rad_pos));
    
    // Draw label
    this._d.fill = this.marker_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(20);
    this._d.stroke = 'transparent';
    this._d.font_weight = 'bold';
    var s = rad_pos / (2*Math.PI);
    switch (true) {
      case s >= 0.96 || s < 0.04  :   this._d.gravity = 'south';      break;
      case s >= 0.04 && s < 0.21  :   this._d.gravity = 'west';       break;  // southwest
      case s >= 0.21 && s < 0.29  :   this._d.gravity = 'west';       break;
      case s >= 0.29 && s < 0.46  :   this._d.gravity = 'west';       break;  // northwest
      case s >= 0.46 && s < 0.54  :   this._d.gravity = 'north';      break;
      case s >= 0.54 && s < 0.71  :   this._d.gravity = 'east';       break;  // northeast
      case s >= 0.71 && s < 0.79  :   this._d.gravity = 'east';       break;
      case s >= 0.79 && s < 0.96  :   this._d.gravity = 'east';       break;  // southest
    }
    this._d.annotate_scaled(0, 0, x, y, amount, this._scale);
  }
});


// Here's how to make a Pie graph:
//
//   g = new Pie('canvasId');
//   g.title = "Visual Pie Graph Test";
//   g.data('Fries', 20);
//   g.data('Hamburgers', 50);
//   g.draw();
//
// To control where the pie chart starts creating slices, use #zero_degree.
exports.Pie = new Class(Base, {
  extend: {
    TEXT_OFFSET_PERCENTAGE: 0.15
  },
  
  // Can be used to make the pie start cutting slices at the top (-90.0)
  // or at another angle. Default is 0.0, which starts at 3 o'clock.
  zero_degreee: null,
  
  initialize_ivars: function() {
    this.callSuper();
    this.zero_degree = 0.0;
  },
  
  draw: function() {
    this.hide_line_markers = true;
    
    this.callSuper();
    
    if (!this._has_data) return;
    
    var diameter = this._graph_height,
        radius = (Math.min(this._graph_width, this._graph_height) / 2) * 0.8,
        top_x = this._graph_left + (this._graph_width - diameter) / 2,
        center_x = this._graph_left + (this._graph_width / 2),
        center_y = this._graph_top + (this._graph_height / 2) - 10, // Move graph up a bit
        total_sum = this._sums_for_pie(),
        prev_degrees = this.zero_degree,
        index = this.klass.DATA_VALUES_INDEX;
    
    // Use full data since we can easily calculate percentages
    if (this.sort) this._data.sort(function(a,b) { return a[index][0] - b[index][0]; });
    each(this._data, function(data_row, i) {
      if (data_row[this.klass.DATA_VALUES_INDEX][0] > 0) {
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        
        var current_degrees = (data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 360;
        
        // Gruff uses ellipse() here, but canvas doesn't seem to support it.
        // circle() is fine for our purposes here.
        this._d.circle(center_x, center_y,
                    center_x + radius, center_y,
                    prev_degrees, prev_degrees + current_degrees + 0.5); // <= +0.5 'fudge factor' gets rid of the ugly gaps
        
        var half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2;
        
        var label_string = Math.round((data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) *
                              100.0) + '%';
        this._draw_label(center_x, center_y, half_angle,
                          radius + (radius * this.klass.TEXT_OFFSET_PERCENTAGE),
                          label_string);
        
        prev_degrees += current_degrees;
      }
    }, this);
    
    // TODO debug a circle where the text is drawn...
  },
  
  // Labels are drawn around a slightly wider ellipse to give room for 
  // labels on the left and right.
  _draw_label: function(center_x, center_y, angle, radius, amount) {
    // TODO Don't use so many hard-coded numbers
    var r_offset = 20.0,      // The distance out from the center of the pie to get point
        x_offset = center_x,  // + 15.0 # The label points need to be tweaked slightly
        y_offset = center_y,  // This one doesn't though
        radius_offset = radius + r_offset,
        ellipse_factor = radius_offset * 0.15,
        x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle * Math.PI/180)),
        y = y_offset + (radius_offset * Math.sin(angle * Math.PI/180));
    
    // Draw label
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.marker_font_size);
    this._d.font_weight = 'bold';
    this._d.gravity = 'center';
    this._d.annotate_scaled(0,0, x,y, amount, this._scale);
  },
  
  _sums_for_pie: function() {
    var total_sum = 0;
    each(this._data, function(data_row) {
      total_sum += data_row[this.klass.DATA_VALUES_INDEX][0];
    }, this);
    return total_sum;
  }
});


// Graph with individual horizontal bars instead of vertical bars.

exports.SideBar = new Class(Base, {

  draw: function() {
    this.has_left_labels = true;
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Setup spacing.
    //
    var spacing_factor = 0.9;
    
    this._bars_width = this._graph_height / this._column_count;
    this._bar_width = this._bars_width * spacing_factor / this._norm_data.length;
    this._d.stroke_opacity = 0.0;
    var height = [], i = this._column_count;
    while (i--) height[i] = 0;
    var length = [], j = this._column_count;
    while (j--) length[j] = this._graph_left;
    var padding = (this._bars_width * (1 - spacing_factor)) / 2;
    
    each(this._norm_data, function(data_row, row_index) {
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        
        // Using the original calcs from the stacked bar chart
        // to get the difference between
        // part of the bart chart we wish to stack.
        var temp1      = this._graph_left + (this._graph_width - data_point * this._graph_width - height[point_index]),
            temp2      = this._graph_left + this._graph_width - height[point_index],
            difference = temp2 - temp1,
        
            left_x     = length[point_index] - 1,
            left_y     = this._graph_top + (this._bars_width * point_index) + (this._bar_width * row_index) + padding,
            right_x    = left_x + difference,
            right_y    = left_y + this._bar_width;
        
        height[point_index] += (data_point * this._graph_width);
        
        this._d.stroke = 'transparent';
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.rectangle(left_x, left_y, right_x, right_y);
        
        // Calculate center based on bar_width and current row
        var label_center = this._graph_top + (this._bars_width * point_index + this._bars_width / 2) + padding;
        this._draw_label(label_center, point_index);
      }, this)
      
    }, this);
  },
  
  // Instead of base class version, draws vertical background lines and label
  _draw_line_markers: function() {
    
    if (this.hide_line_markers) return;
    
    // Draw horizontal line markers and annotate with numbers
    this._d.stroke_width = 1;
    var number_of_lines = 5;
    
    // TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
    var increment = this._significant(this.maximum_value / number_of_lines),
        line_diff, x, diff, marker_label;
    for (var index = 0; index <= number_of_lines; index++) {
      
      line_diff    = (this._graph_right - this._graph_left) / number_of_lines;
      x            = this._graph_right - (line_diff * index) - 1;
      diff         = index - number_of_lines;
      marker_label = Math.abs(diff) * increment;
      
      this._d.stroke = this.marker_color;
      this._d.line(x, this._graph_bottom, x, this._graph_top);
      
      if (!this.hide_line_numbers) {
        this._d.fill      = this.font_color;
        if (this.font) this._d.font = this.font;
        this._d.stroke    = 'transparent';
        this._d.pointsize = this._scale_fontsize(this.marker_font_size);
        this._d.gravity   = 'center';
        // TODO Center text over line
        this._d.annotate_scaled(
                          0, 0, // Width of box to draw text in
                          x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
                          marker_label, this._scale);
      }
    }
  },
  
  // Draw on the Y axis instead of the X
  _draw_label: function(y_offset, index) {
    if (this.labels[index] && !this._labels_seen[index]) {
      this._d.fill             = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke           = 'transparent';
      this._d.font_weight      = 'normal';
      this._d.pointsize        = this._scale_fontsize(this.marker_font_size);
      this._d.gravity          = 'east';
      this._d.annotate_scaled(
                              1, 1,
                              this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
                              this.labels[index], this._scale);
      this._labels_seen[index] = true;
    }
  }
});


// Experimental!!! See also the Net graph.
//
// Submitted by Kevin Clark http://glu.ttono.us/
exports.Spider = new Class(Base, {
  
  // Hide all text
  hide_text: null,
  hide_axes: null,
  transparent_background: null,
  
  initialize: function(renderer, max_value, target_width) {
    this.callSuper(renderer, target_width);
    this._max_value = max_value;
    this.hide_legend = true;
  },
  
  draw: function() {
    this.hide_line_markers = true;
    
    this.callSuper();

    if (!this._has_data) return;
    
    // Setup basic positioning
    var diameter = this._graph_height,
        radius = this._graph_height / 2.0,
        top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
        center_x = this._graph_left + (this._graph_width / 2.0),
        center_y = this._graph_top + (this._graph_height / 2.0) - 25; // Move graph up a bit
    
    this._unit_length = radius / this._max_value;
    
    var total_sum = this._sums_for_spider(),
        prev_degrees = 0.0,
        additive_angle = (2 * Math.PI) / this._data.length,
        
        current_angle = 0.0;
    
    // Draw axes
    if (!this.hide_axes) this._draw_axes(center_x, center_y, radius, additive_angle);
    
    // Draw polygon
    this._draw_polygon(center_x, center_y, additive_angle);
  },
  
  _normalize_points: function(value) {
    return value * this._unit_length;
  },
  
  _draw_label: function(center_x, center_y, angle, radius, amount) {
    var r_offset = 50,            // The distance out from the center of the pie to get point
        x_offset = center_x,      // The label points need to be tweaked slightly
        y_offset = center_y + 0,  // This one doesn't though
        x = x_offset + ((radius + r_offset) * Math.cos(angle)),
        y = y_offset + ((radius + r_offset) * Math.sin(angle));
    
    // Draw label
    this._d.fill = this.marker_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.legend_font_size);
    this._d.stroke = 'transparent';
    this._d.font_weight = 'bold';
    this._d.gravity = 'center';
    this._d.annotate_scaled(
                      0, 0,
                      x, y, 
                      amount, this._scale);
  },
  
  _draw_axes: function(center_x, center_y, radius, additive_angle, line_color) {
    if (this.hide_axes) return;
    
    var current_angle = 0.0;
    
    each(this._data, function(data_row) {
      this._d.stroke = line_color || data_row[this.klass.DATA_COLOR_INDEX];
      this._d.stroke_width = 5.0;
      
      var x_offset = radius * Math.cos(current_angle);
      var y_offset = radius * Math.sin(current_angle);
      
      this._d.line(center_x, center_y,
              center_x + x_offset,
              center_y + y_offset);
      
      if (!this.hide_text) this._draw_label(center_x, center_y, current_angle, radius, data_row[this.klass.DATA_LABEL_INDEX]);
      
      current_angle += additive_angle;
    }, this);
  },
  
  _draw_polygon: function(center_x, center_y, additive_angle, color) {
    var points = [],
        current_angle = 0.0;
    each(this._data, function(data_row) {
      points.push(center_x + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.cos(current_angle));
      points.push(center_y + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.sin(current_angle));
      current_angle += additive_angle;
    }, this);
    
    this._d.stroke_width = 1.0;
    this._d.stroke = color || this.marker_color;
    this._d.fill = color || this.marker_color;
    this._d.fill_opacity = 0.4;
    this._d.polyline(points);
  },
  
  _sums_for_spider: function() {
    var sum = 0.0;
    each(this._data, function(data_row) {
      sum += data_row[this.klass.DATA_VALUES_INDEX][0];
    }, this);
    return sum;
  }
});


// Used by StackedBar and child classes.
exports.Base.StackedMixin = new Module({
  // Get sum of each stack
  _get_maximum_by_stack: function() {
    var max_hash = {};
    each(this._data, function(data_set) {
      each(data_set[this.klass.DATA_VALUES_INDEX], function(data_point, i) {
        if (!max_hash[i]) max_hash[i] = 0.0;
        max_hash[i] += data_point;
      }, this);
    }, this);
    
    // this.maximum_value = 0;
    for (var key in max_hash) {
      if (max_hash[key] > this.maximum_value) this.maximum_value = max_hash[key];
    }
    this.minimum_value = 0;
  }
});


exports.StackedArea = new Class(Base, {
  include: Base.StackedMixin,
  last_series_goes_on_bottom: null,
  
  draw: function() {
    this._get_maximum_by_stack();
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    this._d.stroke = 'transparent';
    
    var height = [], i = this._column_count;
    while (i--) height.push(0);
    
    var data_points = null;
    var iterator = this.last_series_goes_on_bottom ? reverse_each : each;
    iterator(this._norm_data, function(data_row) {
      var prev_data_points = data_points;
      data_points = [];
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        // Use incremented x and scaled y
        var new_x = this._graph_left + (this._x_increment * index);
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height - height[index]);
        
        height[index] += (data_point * this._graph_height);
        
        data_points.push(new_x);
        data_points.push(new_y);
        
        this._draw_label(new_x, index);
      }, this);
      
      var poly_points, i, n;
      
      if (prev_data_points) {
        poly_points = array(data_points);
        for (i = prev_data_points.length/2 - 1; i >= 0; i--) {
          poly_points.push(prev_data_points[2*i]);
          poly_points.push(prev_data_points[2*i+1]);
        }
        poly_points.push(data_points[0]);
        poly_points.push(data_points[1]);
      } else {
        poly_points = array(data_points);
        poly_points.push(
          this._graph_right,
          this._graph_bottom - 1,
          this._graph_left,
          this._graph_bottom - 1,
          data_points[0],
          data_points[1]
        );
      }
      this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
      this._d.polyline(poly_points);
    }, this);
  }
});


exports.StackedBar = new Class(Base, {
  include: Base.StackedMixin,

  // Draws a bar graph, but multiple sets are stacked on top of each other.
  draw: function() {
    this._get_maximum_by_stack();
    this.callSuper();
    if (!this._has_data) return;
    
    // Setup spacing.
    //
    // Columns sit stacked.
    var spacing_factor = 0.9;
    this._bar_width = this._graph_width / this._column_count;
    var padding = (this._bar_width * (1 - spacing_factor)) / 2;
    
    var height = [], i = this._column_count;
    while (i--) height.push(0);
  
    each(this._norm_data, function(data_row, row_index) {
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        // Calculate center based on bar_width and current row
        var label_center = this._graph_left + (this._bar_width * point_index) + (this._bar_width * spacing_factor / 2.0) + padding;
        this._draw_label(label_center, point_index);
        
        if (data_point == 0) return;
        // Use incremented x and scaled y
        var left_x = this._graph_left + (this._bar_width * point_index) + padding;
        var left_y = this._graph_top + (this._graph_height -
                               data_point * this._graph_height - 
                               height[point_index]) + 1;
        var right_x = left_x + this._bar_width * spacing_factor;
        var right_y = this._graph_top + this._graph_height - height[point_index] - 1;
        
        // update the total height of the current stacked bar
        height[point_index] += (data_point * this._graph_height);
        
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.rectangle(left_x, left_y, right_x, right_y);
      }, this);
    }, this);
  }
});


// A special bar graph that shows a single dataset as a set of
// stacked bars. The bottom bar shows the running total and 
// the top bar shows the new value being added to the array.

exports.AccumulatorBar = new Class(StackedBar, {
  
  draw: function() {
    if (this._data.length != 1) throw 'Incorrect number of datasets exception';
    
    var accumulator_array = [];
    var index = 0;
    var increment_array = [];
    
    each(this._data[0][this.klass.DATA_VALUES_INDEX], function(value) {
      var max = -Infinity;
      each(increment_array, function(x) { max = Math.max(max, x); });
      
      increment_array.push((index > 0) ? (value + max) : value);
      accumulator_array.push(increment_array[index] - value);
      index += 1;
    }, this);
    
    this.data("Accumulator", accumulator_array);
    
    this.callSuper();
  }
});


// New gruff graph type added to enable sideways stacking bar charts 
// (basically looks like a x/y flip of a standard stacking bar chart)
//
// alun.eyre@googlemail.com
exports.SideStackedBar = new Class(SideBar, {
  include: Base.StackedMixin,
  
  draw: function() {
    this.has_left_labels = true;
    this._get_maximum_by_stack();
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Setup spacing.
    //
    // Columns sit stacked.
    var spacing_factor = 0.9;
    
    this._bar_width = this._graph_height / this._column_count;
    var height = [], i = this._column_count,
        length = [], j = this._column_count,
        padding = (this._bar_width * (1 - spacing_factor)) / 2;
    
    while (i--) height.push(0);
    while (j--) length.push(this._graph_left);

    each(this._norm_data, function(data_row, row_index) {
      this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
      
      each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        
    	  // using the original calcs from the stacked bar chart to get the difference between
    	  // part of the bart chart we wish to stack.
    	  var temp1 = this._graph_left + (this._graph_width -
                                    data_point * this._graph_width - 
                                    height[point_index]) + 1;
    	  var temp2 = this._graph_left + this._graph_width - height[point_index] - 1;
    	  var difference = temp2 - temp1;
        
    	  var left_x = length[point_index], //+ 1
            left_y = this._graph_top + (this._bar_width * point_index) + padding,
    	      right_x = left_x + difference,
            right_y = left_y + this._bar_width * spacing_factor;
    	  length[point_index] += difference;
        height[point_index] += (data_point * this._graph_width - 2);
        
        this._d.rectangle(left_x, left_y, right_x, right_y);
        
        // Calculate center based on bar_width and current row
        var label_center = this._graph_top + (this._bar_width * point_index) + (this._bar_width * spacing_factor / 2.0) + padding;
        this._draw_label(label_center, point_index);
      }, this);
    }, this);
  },
  
  _larger_than_max: function(data_point, index) {
    index = index || 0;
    return this._max(data_point, index) > this.maximum_value;
  },
  
  _max: function(data_point, index) {
    var sum = 0;
    each(this._data, function(item) {
      sum += item[this.klass.DATA_VALUES_INDEX][index];
    }, this);
    return sum;
  }
});


exports.Mini = {};

exports.Mini.Legend = new Module({
  
  // The canvas needs to be bigger so we can put the legend beneath it.
  _expand_canvas_for_vertical_legend: function() {
    this._original_rows = this._raw_rows;
    this._rows += this._data.length * this._calculate_caps_height(this._scale_fontsize(this.legend_font_size)) * 1.7;
    this._render_background();
  },
  
  // Draw the legend beneath the existing graph.
  _draw_vertical_legend: function() {
            
    this._legend_labels = [];
    each(this._data, function(item) {
      this._legend_labels.push(item[this.klass.DATA_LABEL_INDEX]);
    }, this);
    
    var legend_square_width = 40.0, // small square with color of this item
        legend_square_margin = 10.0,
        legend_left_margin = 100.0,
        legend_top_margin = 40.0;

    // May fix legend drawing problem at small sizes
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this.legend_font_size;

    var current_x_offset = legend_left_margin,
        current_y_offset = this._original_rows + legend_top_margin;

    this._debug(function() {
      this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
    });

    each(this._legend_labels, function(legend_label, index) {

      // Draw label
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.pointsize = this._scale_fontsize(this.legend_font_size);
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.gravity = 'west';
      this._d.annotate_scaled(this._raw_columns, 1.0,
                        current_x_offset + (legend_square_width * 1.7), current_y_offset, 
                        this._truncate_legend_label(legend_label), this._scale);

      // Now draw box with color of this dataset
      this._d.stroke = 'transparent';
      this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
      this._d.rectangle(current_x_offset, 
                        current_y_offset - legend_square_width / 2.0, 
                        current_x_offset + legend_square_width, 
                        current_y_offset + legend_square_width / 2.0);
      
      current_y_offset += this._calculate_caps_height(this.legend_font_size) * 1.7;
    }, this);
    this._color_index = 0;
  },

  // Shorten long labels so they will fit on the canvas.
  _truncate_legend_label: function(label) {
    var truncated_label = String(label);
    while (this._calculate_width(this._scale_fontsize(this.legend_font_size), truncated_label) > (this._columns - this.legend_left_margin - this.right_margin) && (truncated_label.length > 1))
      truncated_label = truncated_label.substr(0, truncated_label.length-1);
    return truncated_label + (truncated_label.length < String(label).length ? "…" : '');
  }
});


// Makes a small bar graph suitable for display at 200px or even smaller.
//
exports.Mini.Bar = new Class(Bar, {
  include: Mini.Legend,
      
  draw: function() {
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;

    this.marker_font_size = 50.0;
    this.minimum_value = 0.0;
    this.legend_font_size = 60.0;

    this._expand_canvas_for_vertical_legend();

    this.callSuper();

    this._draw_vertical_legend();
  }
});


// Makes a small pie graph suitable for display at 200px or even smaller.
//
exports.Mini.Pie = new Class(Pie, {
  include: Mini.Legend,
  
  initialize_ivars: function() {
    this.callSuper();
    
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;
    
    this.marker_font_size = 60.0;
    this.legend_font_size = 60.0;
  },
  
  draw: function() {
    this._expand_canvas_for_vertical_legend();
    
    this.callSuper();
    
    this._draw_vertical_legend();
  }
});


// Makes a small pie graph suitable for display at 200px or even smaller.
//
exports.Mini.SideBar = new Class(SideBar, {
  include: Mini.Legend,
        
  initialize_ivars: function() {
    this.callSuper();
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;

    this.marker_font_size = 50.0;
    this.legend_font_size = 50.0;
  },
  
  draw: function() {
    this._expand_canvas_for_vertical_legend();
    
    this.callSuper();
    
    this._draw_vertical_legend();
  }
});


exports.Renderer = new Class({
  extend: {
    WRAPPER_CLASS:  'bluff-wrapper',
    TEXT_CLASS:     'bluff-text'
  },

  font:     'Arial, Helvetica, Verdana, sans-serif',
  gravity:  'north',
  
  initialize: function(canvasId) {
    this._canvas = document.getElementById(canvasId);
    this._ctx = this._canvas.getContext('2d');
  },
  
  scale: function(sx, sy) {
    this._sx = sx;
    this._sy = sy || sx;
  },
  
  caps_height: function(font_size) {
    var X = this._sized_text(font_size, 'X'),
        height = this._element_size(X).height;
    this._remove_text_node(X);
    return height;
  },
  
  text_width: function(font_size, text) {
    var element = this._sized_text(font_size, text);
    var width = this._element_size(element).width;
    this._remove_text_node(element);
    return width;
  },
  
  get_type_metrics: function(text) {
    var node = this._sized_text(this.pointsize, text);
    var size = this._element_size(node);
    this._remove_text_node(node);
    return size;
  },
  
  clear: function(width, height) {
    this._canvas.width = width;
    this._canvas.height = height;
    this._ctx.clearRect(0, 0, width, height);
    var wrapper = this._text_container(), children = wrapper.childNodes, i = children.length;
    wrapper.style.width = width + 'px';
    wrapper.style.height = height + 'px';
    while (i--) {
      if (children[i] && children[i].className == this.klass.TEXT_CLASS)
        this._remove_text_node(children[i]);
    }
  },
  
  push: function() {
    this._ctx.save();
  },
  
  pop: function() {
    this._ctx.restore();
  },
  
  render_gradiated_background: function(width, height, top_color, bottom_color) {
    this.clear(width, height);
    var gradient = this._ctx.createLinearGradient(0,0, 0,height);
    gradient.addColorStop(0, top_color);
    gradient.addColorStop(1, bottom_color);
    this._ctx.fillStyle = gradient;
    this._ctx.fillRect(0, 0, width, height);
  },
  
  render_solid_background: function(width, height, color) {
    this.clear(width, height);
    this._ctx.fillStyle = color;
    this._ctx.fillRect(0, 0, width, height);
  },
  
  annotate_scaled: function(width, height, x, y, text, scale) {
    var scaled_width = (width * scale) >= 1 ? (width * scale) : 1;
    var scaled_height = (height * scale) >= 1 ? (height * scale) : 1;
    var text = this._sized_text(this.pointsize, text);
    text.style.color = this.fill;
    text.style.fontWeight = this.font_weight;
    text.style.textAlign = 'center';
    text.style.left = (this._sx * x + this._left_adjustment(text, scaled_width)) + 'px';
    text.style.top = (this._sy * y + this._top_adjustment(text, scaled_height)) + 'px';
  },
  
  circle: function(origin_x, origin_y, perim_x, perim_y, arc_start, arc_end) {
    var radius = Math.sqrt(Math.pow(perim_x - origin_x, 2) + Math.pow(perim_y - origin_y, 2));
    this._ctx.fillStyle = this.fill;
    this._ctx.beginPath();
    var alpha = (arc_start || 0) * Math.PI/180;
    var beta = (arc_end || 360) * Math.PI/180;
    if (arc_start !== undefined && arc_end !== undefined) {
      this._ctx.moveTo(this._sx * (origin_x + radius * Math.cos(beta)), this._sy * (origin_y + radius * Math.sin(beta)));
      this._ctx.lineTo(this._sx * origin_x, this._sy * origin_y);
      this._ctx.lineTo(this._sx * (origin_x + radius * Math.cos(alpha)), this._sy * (origin_y + radius * Math.sin(alpha)));
    }
    this._ctx.arc(this._sx * origin_x, this._sy * origin_y, this._sx * radius, alpha, beta, false);
    this._ctx.fill();
  },
  
  line: function(sx, sy, ex, ey) {
    this._ctx.strokeStyle = this.stroke;
    this._ctx.lineWidth = this.stroke_width;
    this._ctx.beginPath();
    this._ctx.moveTo(this._sx * sx, this._sy * sy);
    this._ctx.lineTo(this._sx * ex, this._sy * ey);
    this._ctx.stroke();
  },
  
  polyline: function(points) {
    this._ctx.fillStyle = this.fill;
    this._ctx.globalAlpha = this.fill_opacity || 1;
    try { this._ctx.strokeStyle = this.stroke; } catch (e) {}
    var x = points.shift(), y = points.shift();
    this._ctx.beginPath();
    this._ctx.moveTo(this._sx * x, this._sy * y);
    while (points.length > 0) {
      x = points.shift(); y = points.shift();
      this._ctx.lineTo(this._sx * x, this._sy * y);
    }
    this._ctx.fill();
  },
  
  rectangle: function(ax, ay, bx, by) {
    var temp;
    if (ax > bx) { temp = ax; ax = bx; bx = temp; }
    if (ay > by) { temp = ay; ay = by; by = temp; }
    try {
      this._ctx.fillStyle = this.fill;
      this._ctx.fillRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
    } catch (e) {}
    try {
      this._ctx.strokeStyle = this.stroke;
      if (this.stroke != 'transparent')
        this._ctx.strokeRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
    } catch (e) {}
  },
  
  _left_adjustment: function(node, width) {
    var w = this._element_size(node).width;
    switch (this.gravity) {
      case 'west':    return 0;
      case 'east':    return width - w;
      case 'north': case 'south': case 'center':
        return (width - w) / 2;
    }
  },
  
  _top_adjustment: function(node, height) {
    var h = this._element_size(node).height;
    switch (this.gravity) {
      case 'north':   return 0;
      case 'south':   return height - h;
      case 'west': case 'east': case 'center':
        return (height - h) / 2;
    }
  },
  
  _text_container: function() {
    var wrapper = this._canvas.parentNode;
    if (wrapper.className == this.klass.WRAPPER_CLASS) return wrapper;
    wrapper = document.createElement('div');
    wrapper.className = this.klass.WRAPPER_CLASS;
    
    wrapper.style.position = 'relative';
    wrapper.style.border = 'none';
    wrapper.style.padding = '0 0 0 0';
    
    this._canvas.parentNode.insertBefore(wrapper, this._canvas);
    wrapper.appendChild(this._canvas);
    return wrapper;
  },
  
  _sized_text: function(size, content) {
    var text = this._text_node(content);
    text.style.fontFamily = this.font;
    text.style.fontSize = (typeof size == 'number') ? size + 'px' : size;
    return text;
  },
  
  _text_node: function(content) {
    var div = document.createElement('div');
    div.className = this.klass.TEXT_CLASS;
    div.style.position = 'absolute';
    div.appendChild(document.createTextNode(content));
    this._text_container().appendChild(div);
    return div;
  },
  
  _remove_text_node: function(node) {
    node.parentNode.removeChild(node);
  },
  
  _element_size: function(element) {
    var display = element.style.display;
    return (display && display != 'none')
        ? {width: element.offsetWidth, height: element.offsetHeight}
        : {width: element.clientWidth, height: element.clientHeight};
  }
});


exports.TableReader = new Class({
  
  NUMBER_FORMAT: /\-?(0|[1-9]\d*)(\.\d+)?(e[\+\-]?\d+)?/i,
  
  initialize: function(table, transpose) {
    this._table = (typeof table == 'string')
        ? document.getElementById(table)
        : table;
    this._swap = !!transpose;
  },
  
  // Get array of data series from the table
  get_data: function() {
    if (!this._data) this._read();
    return this._data;
  },
  
  // Get set of axis labels to use for the graph
  get_labels: function() {
    if (!this._labels) this._read();
    return this._labels;
  },
  
  // Get the title from the table's caption
  get_title: function() {
    return this._title;
  },
  
  // Return series number i
  get_series: function(i) {
    if (this._data[i]) return this._data[i];
    return this._data[i] = {points: []};
  },
  
  // Gather data by reading from the table
  _read: function() {
    this._row = this._col = 0;
    this._row_offset = this._col_offset = 0;
    this._data = [];
    this._labels = {};
    this._row_headings = [];
    this._col_headings = [];
    
    this._walk(this._table);
    
    if ((this._row_headings.length > 1 && this._col_headings.length == 1) ||
        this._row_headings.length < this._col_headings.length) {
      if (!this._swap) this._transpose();
    } else {
      if (this._swap) this._transpose();
    }
    
    each(this._col_headings, function(heading, i) {
      this.get_series(i - this._col_offset).name = heading;
    }, this);
    
    each(this._row_headings, function(heading, i) {
      this._labels[i - this._row_offset] = heading;
    }, this);
  },
  
  // Walk the table's DOM tree
  _walk: function(node) {
    this._visit(node);
    var i, children = node.childNodes, n = children.length;
    for (i = 0; i < n; i++) this._walk(children[i]);
  },
  
  // Read a single DOM node from the table
  _visit: function(node) {
    if (!node.tagName) return;
    var content = this._strip_tags(node.innerHTML), x, y;
    switch (node.tagName.toUpperCase()) {
    
      case 'TR':
        if (!this._has_data) this._row_offset = this._row;
        this._row += 1;
        this._col = 0;
        break;
      
      case 'TD':
        if (!this._has_data) this._col_offset = this._col;
        this._col += 1;
        content = parseFloat(content.match(this.NUMBER_FORMAT)[0]);
        if (typeof content == 'number') {
          this._has_data = true;
          x = this._col - this._col_offset - 1;
          y = this._row - this._row_offset - 1;
          this.get_series(x).points[y] = parseFloat(content);
        }
        break;
      
      case 'TH':
        this._col += 1;
        if (this._col == 1 && this._row == 1)
          this._row_headings[0] = this._col_headings[0] = content;
        else if (node.scope == "row" || this._col == 1)
          this._row_headings[this._row - 1] = content;
        else
          this._col_headings[this._col - 1] = content;
        break;
      
      case 'CAPTION':
        this._title = content;
        break;
    }
  },
  
  // Transpose data in memory
  _transpose: function() {
    var data = this._data, tmp;
    this._data = [];
    
    each(data, function(row, i) {
      each(row.points, function(point, p) {
        this.get_series(p).points[i] = point;
      }, this);
    }, this);
    
    tmp = this._row_headings;
    this._row_headings = this._col_headings;
    this._col_headings = tmp;
    
    tmp = this._row_offset;
    this._row_offset = this._col_offset;
    this._col_offset = tmp;
  },
  
  // Remove HTML from a string
  _strip_tags: function(string) {
    return string.replace(/<\/?[^>]+>/gi, '');
  },
  
  extend: {
    Mixin: new Module({
      data_from_table: function(table, transpose) {
        var reader    = new TableReader(table, transpose),
            data_rows = reader.get_data();
        
        each(data_rows, function(row) {
          this.data(row.name, row.points);
        }, this);
        
        this.labels = reader.get_labels();
        this.title  = reader.get_title() || this.title;
      }
    })
  }
});

exports.Base.include(TableReader.Mixin);

/*license

    Legal
    =======
    
    Chiron is a component of the Tale web-game project.
    
    See <credit.txt> for a complete list of
    contributions and their licenses.  All contributions are provided
    under permissive, non-viral licenses including MIT, BSD, Creative Commons
    Attribution 2.5, Public Domain, or Unrestricted.
    
    
    License
    =======
    
    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    
    MIT License
    -----------
    
    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:
    
    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

*/

